| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373 |
- // @ts-nocheck
- import { useQuery } from '@tanstack/react-query'
- import { IS_PLATFORM, useFeatureFlags } from 'common'
- import { Database } from 'common/marketplace.types'
- import { Search } from 'lucide-react'
- import { useRouter } from 'next/router'
- import { parseAsString, useQueryState } from 'nuqs'
- import { useMemo, type ReactNode } from 'react'
- import { ShimmeringLoader } from 'ui-patterns'
- import { Input } from 'ui-patterns/DataInputs/Input'
- import { PageContainer } from 'ui-patterns/PageContainer'
- import {
- PageHeader,
- PageHeaderAside,
- PageHeaderDescription,
- PageHeaderMeta,
- PageHeaderSummary,
- PageHeaderTitle,
- } from 'ui-patterns/PageHeader'
- import { PageSection, PageSectionContent, PageSectionMeta } from 'ui-patterns/PageSection'
- import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
- import {
- IntegrationCard,
- IntegrationLoadingCard,
- } from '@/components/interfaces/Integrations/Landing/IntegrationCard'
- import { IntegrationDefinition } from '@/components/interfaces/Integrations/Landing/Integrations.constants'
- import { useAvailableIntegrations } from '@/components/interfaces/Integrations/Landing/useAvailableIntegrations'
- import { useInstalledIntegrations } from '@/components/interfaces/Integrations/Landing/useInstalledIntegrations'
- import { MarketplaceIndex } from '@/components/interfaces/Integrations/Marketplace/MarketplaceIndex'
- import { DefaultLayout } from '@/components/layouts/DefaultLayout'
- import { ProjectIntegrationsLayoutDispatch } from '@/components/layouts/ProjectIntegrationsLayoutDispatch'
- import { AlertError } from '@/components/ui/AlertError'
- import { DocsButton } from '@/components/ui/DocsButton'
- import { NoSearchResults } from '@/components/ui/NoSearchResults'
- import { useMarketplaceCategoriesQuery } from '@/data/marketplace/integration-categories-query'
- import { BASE_PATH, DOCS_URL } from '@/lib/constants'
- import type { NextPageWithLayout } from '@/types'
- const FEATURED_INTEGRATIONS = ['cron', 'queues', 'stripe_sync_engine']
- // Featured integration images
- const FEATURED_INTEGRATION_IMAGES: Record<string, string> = {
- cron: `${BASE_PATH}/img/integrations/covers/cron-cover.webp`,
- queues: `${BASE_PATH}/img/integrations/covers/queues-cover.png`,
- stripe_wrapper: `${BASE_PATH}/img/integrations/covers/stripe-cover.png`,
- stripe_sync_engine: `${BASE_PATH}/img/integrations/covers/stripe-cover.png`,
- }
- function getIntegrationImage(integration: IntegrationDefinition) {
- let featured_image = FEATURED_INTEGRATION_IMAGES[integration.id]
- if (featured_image) {
- return featured_image
- }
- if (integration.files?.length) {
- const heroImage = integration?.files?.[0]
- return heroImage
- }
- }
- type PageContent = {
- title: string
- subtitle: string
- secondaryActions?: ReactNode
- }
- const DEFAULT_PAGE_CONTENT: PageContent = {
- title: 'Extend your database',
- subtitle:
- 'Extensions and wrappers that add functionality to your database and connect to external services.',
- }
- const CATEGORY_PAGE_CONTENT = {
- wrapper: {
- title: 'Wrappers',
- subtitle:
- 'Connect to external data sources and services by querying APIs, databases, and files as if they were Postgres tables.',
- secondaryActions: (
- <DocsButton href={`${DOCS_URL}/guides/database/extensions/wrappers/overview`} />
- ),
- },
- postgres_extension: {
- title: 'Postgres Modules',
- subtitle: 'Extend your database with powerful Postgres extensions.',
- },
- } satisfies Record<string, PageContent>
- // Converts a category string to title
- // Example: some_catory -> Some Category
- function formatCategoryTitle(category: string) {
- return category
- .split(/[-_]/)
- .filter(Boolean)
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
- .join(' ')
- }
- const PageHeaderContentSkeleton = () => (
- <div className="flex flex-col gap-y-2">
- <ShimmeringLoader className="h-8 w-48" />
- <ShimmeringLoader className="h-4 w-full max-w-xl" />
- </div>
- )
- // Returns selected category to filter by
- function useFilterCategory() {
- const router = useRouter()
- const [selectedCategory] = useQueryState(
- 'category',
- parseAsString.withDefault('all').withOptions({ clearOnDefault: true })
- )
- const categoryFromUrl = useMemo(() => {
- const queryString = router.asPath.split('?')[1]
- if (!queryString) return null
- return new URLSearchParams(queryString).get('category')
- }, [router.asPath])
- const resolvedSelectedCategory = router.isReady
- ? (categoryFromUrl ?? selectedCategory)
- : undefined
- const filterCategory = resolvedSelectedCategory ?? 'all'
- return filterCategory
- }
- // Dynamic page content based on selected category
- function usePageContent(
- integrationFilterCategory: string,
- categories: Database['public']['Views']['categories']['Row'][]
- ) {
- const pageContent = useMemo<PageContent>(() => {
- if (integrationFilterCategory === 'all') {
- return DEFAULT_PAGE_CONTENT
- }
- if (integrationFilterCategory in CATEGORY_PAGE_CONTENT) {
- return CATEGORY_PAGE_CONTENT[integrationFilterCategory as keyof typeof CATEGORY_PAGE_CONTENT]
- }
- const selectedMarketplaceCategory = categories.find(
- (category) => category.slug === integrationFilterCategory
- )
- return {
- title: selectedMarketplaceCategory?.name ?? formatCategoryTitle(integrationFilterCategory),
- subtitle: selectedMarketplaceCategory?.description ?? DEFAULT_PAGE_CONTENT.subtitle,
- }
- }, [categories, integrationFilterCategory])
- return pageContent
- }
- // Filters all available integrations first by category,
- // then by the search term and sorts them first by
- // installation status and then alphabetically
- function useFilteredAndSortedIntegrations(
- availableIntegrations: IntegrationDefinition[],
- selectedCategory: string,
- search: string,
- installedIds: string[]
- ) {
- const filteredAndSortedIntegrations = useMemo(() => {
- let filtered = availableIntegrations ?? []
- if (selectedCategory !== 'all') {
- filtered = filtered.filter(
- (i) => i.type === selectedCategory || i.categories?.includes(selectedCategory)
- )
- }
- if (search.length > 0) {
- filtered = filtered.filter((i) => i.name.toLowerCase().includes(search.toLowerCase()))
- }
- // Sort by installation status, then alphabetically
- return filtered.sort((a, b) => {
- const aIsInstalled = installedIds.includes(a.id)
- const bIsInstalled = installedIds.includes(b.id)
- if (aIsInstalled && !bIsInstalled) return -1
- if (!aIsInstalled && bIsInstalled) return 1
- return a.name.localeCompare(b.name)
- })
- }, [availableIntegrations, selectedCategory, search, installedIds])
- return filteredAndSortedIntegrations
- }
- // Returns featured integrations
- function useFeaturedIntegratios(
- filteredAndSortedIntegrations: IntegrationDefinition[],
- selectedCategory: string,
- search: string
- ) {
- const groupedIntegrations = useMemo(() => {
- if (selectedCategory !== 'all' || search.length > 0) {
- return null
- }
- const featured = filteredAndSortedIntegrations.filter(
- (integration) => FEATURED_INTEGRATIONS.includes(integration.id) || integration.featured
- )
- return featured
- }, [filteredAndSortedIntegrations, selectedCategory, search])
- return groupedIntegrations
- }
- const IntegrationsPage: NextPageWithLayout = () => {
- const isMarketplaceEnabled = useIsMarketplaceEnabled()
- if (isMarketplaceEnabled) return <MarketplaceIndex />
- return <LegacyIntegrationsPage />
- }
- const LegacyIntegrationsPage = () => {
- const { hasLoaded: flagsLoaded } = useFeatureFlags()
- const isMarketplaceEnabled = useIsMarketplaceEnabled()
- const [search, setSearch] = useQueryState(
- 'search',
- parseAsString.withDefault('').withOptions({ clearOnDefault: true })
- )
- const {
- data: availableIntegrations,
- error,
- isPending: isLoadingAvailableIntegrations,
- isError,
- isSuccess: isSuccessAvailableIntegrations,
- } = useAvailableIntegrations()
- const {
- installedIntegrations,
- isLoading: isLoadingInstalledIntegrations,
- isSuccess: isSuccessInstalledIntegrations,
- } = useInstalledIntegrations()
- const installedIds = installedIntegrations.map((i) => i.id)
- const isLoading = isLoadingAvailableIntegrations || isLoadingInstalledIntegrations
- const isSuccess = isSuccessAvailableIntegrations && isSuccessInstalledIntegrations
- const selectedCategory = useFilterCategory()
- const { data: categories = [], isPending: isPendingCategories } = useQuery(
- useMarketplaceCategoriesQuery({ enabled: isMarketplaceEnabled })
- )
- const isLoadingSelectedCategory =
- selectedCategory !== 'all' &&
- !(selectedCategory in CATEGORY_PAGE_CONTENT) &&
- (IS_PLATFORM ? !flagsLoaded || (isMarketplaceEnabled && isPendingCategories) : false)
- const pageContent = usePageContent(selectedCategory, categories)
- const filteredAndSortedIntegrations = useFilteredAndSortedIntegrations(
- availableIntegrations,
- selectedCategory,
- search,
- installedIds
- )
- const featuredIntegrations = useFeaturedIntegratios(
- filteredAndSortedIntegrations,
- selectedCategory,
- search
- )
- return (
- <>
- <PageHeader size="large">
- <PageHeaderMeta>
- <PageHeaderSummary>
- {isLoadingSelectedCategory ? (
- <PageHeaderContentSkeleton />
- ) : (
- <>
- <PageHeaderTitle>{pageContent.title}</PageHeaderTitle>
- <PageHeaderDescription>{pageContent.subtitle}</PageHeaderDescription>
- </>
- )}
- </PageHeaderSummary>
- {pageContent.secondaryActions && (
- <PageHeaderAside>{pageContent.secondaryActions}</PageHeaderAside>
- )}
- </PageHeaderMeta>
- </PageHeader>
- <PageContainer size="large">
- <PageSection>
- <PageSectionMeta>
- <Input
- value={search}
- size="tiny"
- onChange={(e) => setSearch(e.target.value)}
- placeholder="Search integrations..."
- icon={<Search size={14} />}
- className="w-52"
- />
- </PageSectionMeta>
- <PageSectionContent>
- {isLoading && (
- <div
- className="grid xl:grid-cols-3 2xl:grid-cols-4 gap-x-4 gap-y-3"
- style={{ gridAutoRows: 'minmax(110px, auto)' }}
- >
- {Array.from({ length: 8 }).map((_, idx) => (
- <IntegrationLoadingCard key={`integration-loading-${idx}`} />
- ))}
- </div>
- )}
- {/* Error State */}
- {isError && (
- <AlertError subject="Failed to retrieve available integrations" error={error} />
- )}
- {/* Success State */}
- {isSuccess && (
- <>
- {/* No Search Results */}
- {search.length > 0 && filteredAndSortedIntegrations.length === 0 && (
- <NoSearchResults searchString={search} onResetFilter={() => setSearch('')} />
- )}
- {/* Featured Integrations */}
- {featuredIntegrations && featuredIntegrations.length > 0 && (
- <div
- className="grid grid-cols-2 @4xl:grid-cols-3 gap-4 mb-4 items-stretch pb-6 border-b"
- style={{ gridAutoRows: 'minmax(110px, auto)' }}
- >
- {featuredIntegrations.map((integration) => (
- <IntegrationCard
- key={integration.id}
- {...integration}
- isInstalled={installedIds.includes(integration.id)}
- featured={true}
- image={getIntegrationImage(integration)}
- />
- ))}
- </div>
- )}
- {/* All Filtered and Sorted Integrations */}
- {filteredAndSortedIntegrations.length > 0 && (
- <div className="grid @xl:grid-cols-3 @6xl:grid-cols-4 gap-4">
- {filteredAndSortedIntegrations.map((integration) => (
- <IntegrationCard
- key={integration.id}
- {...integration}
- isInstalled={installedIds.includes(integration.id)}
- featured={false}
- image={FEATURED_INTEGRATION_IMAGES[integration.id]}
- />
- ))}
- </div>
- )}
- </>
- )}
- </PageSectionContent>
- </PageSection>
- </PageContainer>
- </>
- )
- }
- IntegrationsPage.getLayout = (page) => (
- <DefaultLayout>
- <ProjectIntegrationsLayoutDispatch>{page}</ProjectIntegrationsLayoutDispatch>
- </DefaultLayout>
- )
- export default IntegrationsPage
|